/*
 * This file is part of "JTA - Telnet/SSH for the JAVA(tm) platform".
 *
 * (c) Matthias L. Jugel, Marcus Meißner 1996-2005. All Rights Reserved.
 *
 * Please visit http://javatelnet.org/ for updates and contact.
 *
 * --LICENSE NOTICE--
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 * --LICENSE NOTICE--
 *
 */

package de.mud.jta.plugin;

import de.mud.jta.FilterPlugin;
import de.mud.jta.Plugin;
import de.mud.jta.PluginBus;
import de.mud.jta.PluginConfig;
import de.mud.jta.VisualPlugin;
import de.mud.jta.event.ConfigurationListener;
import de.mud.jta.event.SocketRequest;
import de.mud.jta.event.TelnetCommandRequest;

import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JMenu;
import javax.swing.JPanel;
import javax.swing.JTextField;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.StreamTokenizer;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;

/**
 * Implementation of a programmable button bar to be used as a plugin in the
 * Java(tm) Telnet Applet/Application. The button bar is configured using a
 * input file that contains the setup for the button bar.
 * <P>
 * A typical setup file may look like:
 * 
 * <PRE>
 * #
 * # Example for writing a button bar config file.
 * #
 * # The syntaxt for defining buttons, input fields and breaks is as follows:
 * #
 * # - defining a button:
 * # A button is defined by giving the keyword 'button' followed by the text
 * # of the button and the command that should be sent when pressing the
 * # button. If the command contains whitespace characters, enclode it in
 * # quote (&quot;) characters!
 * #
 * button		Connect		&quot;\$connect(\@host@,\@port@)&quot;
 * #
 * # - defining a label:
 * # A labvel is defined by giving the keyword 'label' followed by the text
 * # of the label. If the label contains whitespace characters, enclode it in
 * # quote (&quot;) characters!
 * #
 * label 		&quot;Hello User&quot;
 * #
 * # - defining an input field:
 * # An input field is defined just like the button above, but it has one more
 * # parameter, the size of the input field. So you define it, by giving the
 * # keyword 'input' followed by the name of the input field (for reference)
 * # followed by the size of the input field and optionally a third parameter
 * # which is the initial text to be displayed in that field.
 * #
 * input		host	20	&quot;tanis&quot;
 * stretch
 * input		port	4	&quot;23&quot;
 * #
 * # Now after the button and two input fields we define another button which
 * # will be shown last in the row. Order is significant for the order in
 * # which the buttons and fields appear.
 * #
 * button		Disconnect	&quot;\\$disconnect()&quot; break
 * #
 * # To implement an input line that is cleared and sends text use this:
 * # The following line send the text in the input field &quot;send&quot; and appends
 * # a newline.
 * input		send	20	&quot;\\@send@\n&quot;	&quot;ls&quot;
 * #
 * # - Defining a choice
 * # A choice is defined just like the button above, but it has multiple
 * # text/command pairs. If the text or command contain whitespace characters,
 * # enclose them in quote (&quot;) characters. The text and command data may be
 * # spread over several lines for better readability. Make the first command
 * # empty because it is initially selected, and choosing it will have no
 * # effect until some other item has been chosen.
 * #choice       &quot;- choose -&quot;   &quot;&quot;
 * #             &quot;Text 1&quot;       &quot;Command 1&quot;
 * #             &quot;Text 2&quot;       &quot;Command 2&quot;
 * #             &quot;Text 3&quot;       &quot;Command 3&quot;
 * # etc...
 * 
 * </PRE>
 * 
 * Other possible keywords are <TT>break</TT> which does introduce a new line so
 * that buttons and input fields defined next will appear in a new line below
 * and <TT>stretch</TT> to make the just defined button or input field stretched
 * as far as possible on the line. That last keyword is useful to fill the
 * space.
 * 
 * @version $Id: ButtonBar.java 499 2005-09-29 08:24:54Z leo $
 * @author Matthias L. Jugel, Marcus Mei�ner
 */
public class ButtonBar extends Plugin implements FilterPlugin, VisualPlugin,
	ActionListener, ListSelectionListener
{

    /** the panel that contains the buttons and input fields */
    protected JPanel panel = new JPanel();

    // these tables contain our buttons and fields.
    private Map buttons = null;
    private Map choices = null;
    private Map fields = null;

    // the switch for clearing input fields after enter
    private boolean clearFields = true;

    /**
     * Initialize the button bar and register plugin listeners
     */
    public ButtonBar(PluginBus bus, final String id)
    {
	super(bus, id);

	// configure the button bar
	bus.registerPluginListener(new ConfigurationListener()
	{
	    public void setConfiguration(PluginConfig cfg)
	    {
		String file = cfg.getProperty("ButtonBar", id, "setup");
		clearFields = (new Boolean(cfg.getProperty("ButtonBar", id,
			"clearFields"))).booleanValue();

		// check for the setup file
		if (file == null)
		{
		    ButtonBar.this.error("no setup file");
		    return;
		}

		StreamTokenizer setup = null;
		InputStream is = null;

		try
		{
		    is = getClass().getResourceAsStream(file);
		}
		catch (Exception e)
		{
		    // ignore any errors here
		}

		// if the resource access fails, try URL
		if (is == null)
		    try
		    {
			is = new URL(file).openStream();
		    }
		    catch (Exception ue)
		    {
			ButtonBar.this.error("could not find: " + file);
			return;
		    }

		// create a new stream tokenizer to read the file
		try
		{
		    InputStreamReader ir = new InputStreamReader(is);
		    setup = new StreamTokenizer(new BufferedReader(ir));
		}
		catch (Exception e)
		{
		    ButtonBar.this.error("cannot load " + file + ": " + e);
		    return;
		}

		setup.commentChar('#');
		setup.quoteChar('"');

		fields = new HashMap();
		buttons = new HashMap();
		choices = new HashMap();

		GridBagLayout l = new GridBagLayout();
		GridBagConstraints c = new GridBagConstraints();
		panel.setLayout(l);
		c.fill = GridBagConstraints.BOTH;

		int token;
		int ChoiceCount = 0;
		JList list;
		// parse the setup file
		try
		{
		    while ((token = setup.nextToken()) != StreamTokenizer.TT_EOF)
		    {
			switch (token)
			{
			case StreamTokenizer.TT_WORD:
			    // reset the constraints
			    c.gridwidth = 1;
			    c.weightx = 0.0;
			    c.weighty = 0.0;
			    // keyword found, parse arguments
			    if (setup.sval.equals("button"))
			    {
				if ((token = setup.nextToken()) != StreamTokenizer.TT_EOF)
				{
				    String descr = setup.sval;
				    if ((token = setup.nextToken()) != StreamTokenizer.TT_EOF)
				    {
					JButton b = new JButton(descr);
					buttons.put(b, setup.sval);
					b.addActionListener(ButtonBar.this);
					l.setConstraints(b, constraints(c,setup));
					panel.add(b);
				    }
				    else
					ButtonBar.this.error(descr+ ": missing button command");
				}
				else
				    ButtonBar.this.error("unexpected end of file");
			    }
			    else if (setup.sval.equals("label"))
			    {
				if ((token = setup.nextToken()) != StreamTokenizer.TT_EOF)
				{
				    String descr = setup.sval;
				    JLabel b = new JLabel(descr);
				    l.setConstraints(b, constraints(c, setup));
				    panel.add(b);
				}
				else
				    ButtonBar.this.error("unexpected end of file");
				/*
				 * choice - new stuff added APS 07-dec-2001 for
				 * Choice buttons Choice info is held in the
				 * choices hash. There are two sorts of hash
				 * entry: 1) for each choices button on the
				 * terminal, key=choice object, value=ID ("C1.",
				 * "C2.", etc) 2) for each item, key=ID+user's
				 * text (eg "C1.opt1"), value=user's command
				 */

			    }
			    else if (setup.sval.equals("choice"))
			    {
				ChoiceCount++;
				String ident = "C" + ChoiceCount + ".";
				list = new JList();
				choices.put(list, ident);
				list.addListSelectionListener(ButtonBar.this);
				// Choices use ItemListener , not Action
				l.setConstraints(list, constraints(c, setup));
				panel.add(list);
				while ((token = setup.nextToken()) != StreamTokenizer.TT_EOF)
				{
				    if (isKeyword(setup.sval))
				    {// Got command ... Back off.
					setup.pushBack();
					break;
				    }
				    String descr = setup.sval; // This is the
							       // hash key.
				    token = setup.nextToken();
				    if (token == StreamTokenizer.TT_EOF)
				    {
					ButtonBar.this.error("unexpected end of file");
				    }
				    else
				    {
					String value = setup.sval;
					if (isKeyword(value))
					{ // Missing command - complain but
					  // continue
					    System.err.println(descr+ ": missing choice command");
					    setup.pushBack();
					    break;
					}
					System.out.println("choice: name='"+ descr + "', value='" + value);
					list.add(descr, new JLabel(descr));
					choices.put(ident + descr, value);
				    }
				}
				ButtonBar.this.error("choices hash: " + choices);
			    }
			    else if (setup.sval.equals("input"))
			    {
				if ((token = setup.nextToken()) != StreamTokenizer.TT_EOF)
				{
				    String descr = setup.sval;
				    if ((token = setup.nextToken()) == StreamTokenizer.TT_NUMBER)
				    {
					int size = (int) setup.nval;
					String init = "", command = "";
					token = setup.nextToken();
					if (isKeyword(setup.sval))
					    setup.pushBack();
					else
					    command = setup.sval;
					token = setup.nextToken();
					if (isKeyword(setup.sval))
					{
					    setup.pushBack();
					    init = command;
					}
					else
					    init = setup.sval;
					JTextField t = new JTextField(init,
						size);
					if (!init.equals(command))
					{
					    buttons.put(t, command);
					    t.addActionListener(ButtonBar.this);
					}
					fields.put(descr, t);
					l.setConstraints(t, constraints(c,
						setup));
					panel.add(t);
				    }
				    else
					ButtonBar.this.error(descr+ ": missing field size");
				}
				else
				    ButtonBar.this.error("unexpected end of file");
			    }
			    break;
			default:
			    ButtonBar.this.error("syntax error at line "+ setup.lineno());
			}
		    }
		}
		catch (IOException e)
		{
		    ButtonBar.this.error("unexpected error while reading setup: " + e);
		}
		panel.validate();
	    }
	});
    }

    private GridBagConstraints constraints(GridBagConstraints c,
	    StreamTokenizer setup) throws IOException
    {
	if (setup.nextToken() == StreamTokenizer.TT_WORD)
	    if (setup.sval.equals("break"))
		c.gridwidth = GridBagConstraints.REMAINDER;
	    else if (setup.sval.equals("stretch"))
		c.weightx = 1.0;
	    else
		setup.pushBack();
	else
	    setup.pushBack();
	return c;
    }

    public void valueChanged(ListSelectionEvent evt)
    {
	String tmp, ident;
	if ((ident = (String) choices.get(evt.getSource())) != null)
	{
	    // It's a choice - get the text from the selected item
	    JList list = (JList) evt.getSource();
	    tmp = (String) choices.get(ident + list.getSelectedValue());
	    if (tmp != null)
		processEvent(tmp);
	}
    }

    public void actionPerformed(ActionEvent evt)
    {
	String tmp;
	if ((tmp = (String) buttons.get(evt.getSource())) != null)
	    processEvent(tmp);
    }

    private void processEvent(String tmp)
    {
	String cmd = "", function = null;
	int idx = 0, oldidx = 0;
	while ((idx = tmp.indexOf('\\', oldidx)) >= 0 && ++idx <= tmp.length())
	{
	    cmd += tmp.substring(oldidx, idx - 1);
	    switch (tmp.charAt(idx))
	    {
	    case 'b':
		cmd += "\b";
		break;
	    case 'e':
		cmd += "";
		break;
	    case 'n':
		cmd += "\n";
		break;
	    case 'r':
		cmd += "\r";
		break;
	    case '$':
	    {
		int ni = tmp.indexOf('(', idx + 1);
		if (ni < idx)
		{
		    error("ERROR: Function: missing '('");
		    break;
		}
		if (ni == ++idx)
		{
		    error("ERROR: Function: missing name");
		    break;
		}
		function = tmp.substring(idx, ni);
		idx = ni + 1;
		ni = tmp.indexOf(')', idx);
		if (ni < idx)
		{
		    error("ERROR: Function: missing ')'");
		    break;
		}
		tmp = tmp.substring(idx, ni);
		idx = oldidx = 0;
		continue;
	    }
	    case '@':
	    {
		int ni = tmp.indexOf('@', idx + 1);
		if (ni < idx)
		{
		    error("ERROR: Input Field: '@'-End Marker not found");
		    break;
		}
		if (ni == ++idx)
		{
		    error("ERROR: Input Field: no name specified");
		    break;
		}
		String name = tmp.substring(idx, ni);
		idx = ni;
		JTextField t;
		if (fields == null
			|| (t = (JTextField) fields.get(name)) == null)
		{
		    error("ERROR: Input Field: requested input \"" + name
			    + "\" does not exist");
		    break;
		}
		cmd += t.getText();
		if (clearFields)
		    t.setText("");
		break;
	    }
	    default:
		cmd += tmp.substring(idx, ++idx);
	    }
	    oldidx = ++idx;
	}

	if (oldidx <= tmp.length())
	    cmd += tmp.substring(oldidx, tmp.length());

	if (function != null)
	{
	    if (function.equals("break"))
	    {
		bus.broadcast(new TelnetCommandRequest((byte) 243)); // BREAK
		return;
	    }
	    if (function.equals("exit"))
	    {
		try
		{
		    System.exit(0);
		}
		catch (Exception e)
		{
		    error("cannot exit: " + e);
		}
	    }
	    if (function.equals("connect"))
	    {
		String address = null;
		int port = -1;
		try
		{
		    if ((idx = cmd.indexOf(",")) >= 0)
		    {
			try
			{
			    port = Integer.parseInt(cmd.substring(idx + 1, cmd
				    .length()));
			}
			catch (Exception e)
			{
			    port = -1;
			}
			cmd = cmd.substring(0, idx);
		    }
		    if (cmd.length() > 0)
			address = cmd;
		    if (address != null)
			if (port != -1)
			    bus.broadcast(new SocketRequest(address, port));
			else
			    bus.broadcast(new SocketRequest(address, 23));
		    else
			error("connect: no address");
		}
		catch (Exception e)
		{
		    error("connect(): failed");
		    e.printStackTrace();
		}
	    }
	    else if (function.equals("disconnect"))
		bus.broadcast(new SocketRequest());
	    else if (function.equals("detach"))
	    {
		error("detach not implemented yet");
	    }
	    else
		error("ERROR: function not implemented: \"" + function + "\"");
	    return;
	}
	// cmd += tmp.substring(oldidx, tmp.length());
	if (cmd.length() > 0)
	    try
	    {
		write(cmd.getBytes());
	    }
	    catch (IOException e)
	    {
		error("send: " + e);
	    }
    }

    public JComponent getPluginVisual()
    {
	return panel;
    }

    public JMenu getPluginMenu()
    {
	return null;
    }

    FilterPlugin source;

    public void setFilterSource(FilterPlugin source)
    {
	this.source = source;
    }

    public FilterPlugin getFilterSource()
    {
	return source;
    }

    public int read(byte[] b) throws IOException
    {
	return source.read(b);
    }

    public void write(byte[] b) throws IOException
    {
	source.write(b);
    }

    private static boolean isKeyword(String txt)
    {
	return (txt.equals("button") || txt.equals("label")
		|| txt.equals("input") || txt.equals("stretch")
		|| txt.equals("choice") || txt.equals("break"));
    }
}
